<?php

class Noginn_Growl
{
    /**
     * The priority flags
     */
    const PRIORITY_LOW = -2;
    const PRIORITY_MODERATE = -1;
    const PRIORITY_NORMAL = 0;
    const PRIORITY_HIGH = 1;
    const PRIORITY_EMERGENCY = 2;
    
    /**
     * The types of packets
     */
    const TYPE_REGISTRATION = 0;
    const TYPE_NOTIFICATION = 1;
    
    /**
     * The growl network protocol version
     */
    const PROTOCOL_VERSION = 1;
    
    /**
     * The port to use when sending the UDP packet
     */
    const UDP_PORT = 9887;
    
    /**
     * The IP address of the growl server
     *
     * @var string
     */
    public $address;
    
    /**
     * The password of the growl server
     *
     * @var string
     */
    public $password;
    
    /**
     * The application name to register with Growl
     *
     * @var string
     */
    public $application = 'Noginn_Growl';
    
    /**
     * The types of notifications that may be sent
     *
     * @var array
     */
    public $notifications = array();
    
    /**
     * Whether the application has already been registered
     *
     * @var bool
     */
    protected $_registered = false;
    
    /**
     * Constructor
     *
     * Usage:
     * <code>
     * // Setup the Growl client
     * $growl = new Noginn_Growl('127.0.0.1', 'password', 'My Growl App');
     * $growl->addNotification('Alert');
     * $growl->addNotification('Warning', array('enabled' => false));
     *
     * // Send the notifications
     * $growl->notify('Alert', 'Title', 'Message', Noginn_Growl::PRIORITY_HIGH, true);
     * $growl->notify('Warning', 'Title', 'Message', Noginn_Growl::PRIORITY_LOW, false);
     * </code>
     *
     * @param string $address The IP address of the Growl server
     * @param string $password The password of the Growl server
     * @param string $application The application name to register with the Growl server
     * @return void
     */
    public function __construct($address = null, $password = null, $application = null)
    {
        if (!is_null($address)) {
            $this->address = $address;
        }
        if (!is_null($password)) {
            $this->password = $password;
        }
        if (!is_null($application)) {
            $this->application = $application;
        }
    }
    
    /**
     * Set the IP address and password of the Growl server
     *
     * @param string $address 
     * @param string $password 
     * @return void
     */
    public function setAddress($address, $password = '')
    {
        $this->address = $address;
        $this->password = $password;
    }
    
    /**
     * Add a notification supported by the application
     *
     * @param string $name 
     * @param bool $enabled 
     * @return void
     */
    public function addNotification($name, $options = array())
    {
        // Set the default enabled value
        if (!array_key_exists('enabled', $options)) {
            $options['enabled'] = true;
        }
        
        $this->notifications[$name] = $options;
    }
    
    /**
     * Send application registration packet
     *
     * @return void
     */
    protected function _register()
    {
        // Setup
        $data = '';
        $defaultsData = '';
        $defaultsCount = 0;
        $notificationsData = '';
        $notificationsCount = 0;
        
        foreach ($this->notifications as $name => $options) {
            // Encode the notification name
            $name = utf8_encode($name);
            $notificationsData .= pack('n', strlen($name)) . $name;
            
            // Check if the notification type is enabled
            if (array_key_exists('enabled', $options) && $options['enabled']) {
                $defaultsData .= pack('c', $notificationsCount);
                $defaultsCount++;
            }
            $notificationsCount++;
        }
        
        // Encode the application name
        $application = utf8_encode($this->application);
        
        // Data packet
        $data = pack('c2nc2', self::PROTOCOL_VERSION, self::TYPE_REGISTRATION, strlen($application), $notificationsCount, $defaultsCount);
        $data .= $application . $notificationsData . $defaultsData;
        
        // Checksum
        $data .= pack('H32', md5($data . $this->password));
        
        $this->send($data);
        $this->_registered = true;
        
        return true;
    }
    
    /**
     * Send a notification
     *
     * @param string $name The notification name
     * @param string $title The title of the notification
     * @param string $description The description of the notification
     * @param int $priority The priority of the notification
     * @param bool $sticky Whether the notification is sticky or not
     * @return void
     */
    public function notify($name, $title, $description, $priority = self::PRIORITY_NORMAL, $sticky = false)
    {
        if (!array_key_exists($name, $this->notifications)) {
            throw new Zend_Exception('Notification name not registered with the application.');
        }
        
        // Register the application if not done so already
        if (!$this->_registered) {
            $this->_register();
        }
        
        // Encode the data
        $name = utf8_encode($name);
        $title = utf8_encode($title);
        $description = utf8_encode($description);
        $application = utf8_encode($this->application);
        
        // Determine the priority/sticky flags
        $flags = ($priority & 7) * 2;
        if ($priority < 0) {
            $flags |= 8;
        }
        if ($sticky) {
            $flags |= 1;
        }
        
        // Data packet
        $data = pack('c2n5', self::PROTOCOL_VERSION, self::TYPE_NOTIFICATION, $flags, strlen($name), strlen($title), strlen($description), strlen($application));
        $data .= $name . $title . $description . $application;
        
        // Checksum
        $data .= pack('H32', md5($data . $this->password));
        
        $this->send($data);
        
        return true;
    }
    
    /**
     * Send the UDP packet
     *
     * @param string $data
     * @return bool
     */
    public function send($data)
    {
        // Use socket extension if available
        if (function_exists('socket_create') && function_exists('socket_sendto')) {
            $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
            $return = socket_sendto($socket, $data, strlen($data), 0x100, $this->address, self::UDP_PORT);
            
            return $return;
        }
        // Otherwise use fsockopen()
        else if (function_exists('fsockopen')) {
            $socket = fsockopen('udp://' . $this->address, self::UDP_PORT);
            $return = fwrite($socket, $data, strlen($data));
            fclose($socket);
            
            return $return;
        }
    }
}
